package com.lry.basic.redis.distributeLock;

import com.lry.basic.redis.readWriteLock.JedisConnectPoll;
import com.lry.basic.redis.readWriteLock.JedisTemplate;
import com.sun.org.apache.regexp.internal.RE;
import lombok.extern.slf4j.Slf4j;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 互斥
 * 解锁还需系铃人 requestId
 * 过期时间
 *
 * 结构应该用hash，因为需要重入性
 */
@Slf4j
public class DistributeLock {

    public static final String READ_LOCK_PREFIX = "distribute_lock_";
    public static String getLockKey(String name){
        return READ_LOCK_PREFIX + name;
    }
    public static String getUUID(){
        return JedisConnectPoll.JEDIS_CONNECT_POLL_UUID.toString() + ":" + Thread.currentThread().getId();
    }

    public void lock(String name){
        tryLock(name, Long.MAX_VALUE, 30, TimeUnit.SECONDS);
    }

    public void lock(String name, long leaseTime, TimeUnit unit){
        tryLock(name, Long.MAX_VALUE, leaseTime, unit);
    }

    /**
     *
     * @param name 业务名称，加锁唯一key
     * @param waitTime 加锁过程最多消耗的时间，超过这个时间失败
     * @param leaseTime 过期时间
     * @param unit 时间单位
     * @return
     */
    public boolean tryLock(String name, long waitTime, long leaseTime, TimeUnit unit){
        Long waitUntilTime = unit.toMillis(waitTime) + System.currentTimeMillis();
        if(waitUntilTime < 0){
            waitUntilTime = Long.MAX_VALUE;
        }
        Long leastTimeLong = unit.toMillis(leaseTime);
        StringBuilder script = new StringBuilder();
        //如果锁不存在，加锁，设置过期时间，设置重入次数1
        //如果锁存在，判断uuid是不是自己，是的话设置过期时间，设置重入次数+1
        //否则，加锁失败
        script.append("if redis.call('exists',KEYS[1]) ==0 then ")
                    .append("redis.call('hset',KEYS[1],ARGV[2],1);")
                    .append("redis.call('pexpire',KEYS[1],ARGV[1]);")
                .append("return -1;")//代表加锁成功
                .append("end;")

                .append("if redis.call('hexists',KEYS[1],ARGV[2])==1 then ")
                    .append("redis.call('hincrby',KEYS[1],ARGV[2],1);")
                    .append("redis.call('pexpire',KEYS[1],ARGV[1]);")
                .append("return -2;")//代表重入成功
                .append("end;")

                //没有获取锁，返回过期时间
                .append("return redis.call('pttl',KEYS[1]);");

        for(;;){
            if(System.currentTimeMillis() > waitUntilTime){
                log.info("线程"+Thread.currentThread()+"在指定时间获锁失败，lock="+getLockKey(name));
                return false;
            }

            Long res = (Long) JedisTemplate.operate().eval(script.toString(),1,getLockKey(name),leastTimeLong.toString(),getUUID());
            if(res.equals(-1L)){
                log.info("线程"+Thread.currentThread()+"获锁成功，lock="+getLockKey(name));
                //启动看门狗
                Thread t = new Thread(new WatchDog(leastTimeLong+System.currentTimeMillis()-2000,name,getUUID()));
                t.setDaemon(true);
                t.start();

                break;
            }else if(res.equals(-2L)){
                log.info("线程"+Thread.currentThread()+"获锁成功-重入获锁,lock="+getLockKey(name));
                break;
            }else if(res>0){
//                log.info("线程"+Thread.currentThread()+"休眠一会等待别人释放锁,lock="+getLockKey(name));
                try {

                    Thread.sleep(1);
                }catch (InterruptedException e) {
                    log.info("线程"+Thread.currentThread()+"休眠一会等待别人释放锁-出现异常,lock="+getLockKey(name));
                    e.printStackTrace();
                }
            }
        }
        return true;

    }

    /**
     * 释放锁逻辑
     * 根据名字+uuid找count，如果找到了，-1，如果<=0 就释放锁
     * @param name
     */
    public void unlock(String name){
        StringBuilder script = new StringBuilder();
        script.append("local count = redis.call('hget',KEYS[1],KEYS[2]);")
                .append("if count then ")
                    .append("local delCount = redis.call('hincrby', KEYS[1], KEYS[2], -1); ")
                    .append("if tonumber(delCount)<=0 then ")
                        .append("redis.call('HDEL',KEYS[1],KEYS[2]);")
                    .append("end;")
                .append("else ")
                    .append("redis.call('HDEL',KEYS[1],KEYS[2]);")
                .append("end;")
                .append("return;");

        JedisTemplate.operate().eval(script.toString(),2,getLockKey(name),getUUID());
        log.info("线程"+Thread.currentThread()+"释放锁成功,lock="+getLockKey(name));
    }


    class WatchDog implements Runnable{

        private String name;
        private Long future;
        private String uuid;
        public WatchDog(Long future, String name,String uuid) {
            this.name = name;
            this.future = future;
            this.uuid = uuid;
        }

        @Override
        public void run() {
            System.out.println("启动看门狗,lock="+getLockKey(name));
            while(true){
                long cur = System.currentTimeMillis();
                //如果到过期时间节点锁还没有被释放，给锁续期10s
                if(cur>=future){
                    //判断锁存不存在，如果存在就续期10s
                    StringBuilder script = new StringBuilder();

                    script.append("if redis.call('exists',KEYS[1]) ==1 then ")
                            .append("if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then ")
                                .append("redis.call('pexpire',KEYS[1],10000);")
                                .append("return 1;")
                            .append("end;")
                            .append("end;")
                            .append("return 0;");

                    long ret = (long) JedisTemplate.operate().eval(script.toString(),1,getLockKey(name),uuid);
                    if(1==ret){
                        future = System.currentTimeMillis()+10000-2000;
                        log.info("给线程"+Thread.currentThread()+"续期10s,lock="+getLockKey(name));
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}
